API Authorization Design – OAuth Architecture Guidance
Background
In our previous post we discussed User Data Management and in this post we will focus on data protection in APIs. Any real world system needs to apply business rules before allowing access to resources.
OAuth Capabilities
The OAuth family of specifications has many finer details, but I think of these as the two main capabilities:
Capability | Description |
---|---|
Authentication | Dealing with users and authentication, while externalizing this complexity from applications |
Data Protection | Design patterns that enable APIs to authorize access to resources based on tokens |
API clients may use different authentication flows, but APIs always protect data in the same way, by receiving access tokens:
This post will focus on OAuth data protection and how it maps to complex business rules, which is an area that is often not properly understood.
APIs and Business Rules
A request to get data from a UI might look like this, where a token containing the subject claim is sent and then business rules are applied:
The above example might include handling business rules like these, and we will show how to manage these types of rule in an OAuth secured API:
- User Bob has Write Access to orders for customers he manages
- User Bob has View Access to all orders for his own branch
- User Bob has No Access to orders for other branches
API Authorization Steps
Implementing authorization in done via three phases but the main work is always done in step 3:
Step | Description |
---|---|
Token Validation | JWT Access Token Validation, to ensure integrity of the message credential |
Scope Checks | Sanity checks to ensure that an access token is allowed to be used for a particular business area |
Claims Based Authorization | Detailed permission checks against resources, using domain specific data |
Step 1: Token Validation
The JWT Access Token Validation blog post described how an API verifies the JWT’s digital signature and if the token is expired. If the token is not valid a 401 error is returned:
"code": "unauthorized",
"message": "Missing, invalid or expired access token"
Think of token validation as an entry level check to authenticate the request to the API, after which the API can trust data in the access token’s payload.
Step 2: Scopes
Scopes can be included in access tokens to represent an Area of Data and Permissions on that Data.
Examples | Usage |
---|---|
orders | Indicates that an access token grants access to orders placed by a user |
orders_read | Indicates that an access token cannot be used to make data changes to orders |
When personal assets are involved, it is usual to show a consent screen that displays scopes, so that the user knows which data they are granting access to, and whether they are granting read or write access.
Built-In Scopes
OAuth also uses some built in scopes for Personally Identifiable Information (PII) that is stored by the Authorization Server:
Examples | Usage Scenario |
---|---|
openid | Indicates that the user’s identity is being used, via the OpenID Connect protocol |
profile | Indicates that the user’s name and possibly other information is being used |
Scope Limitations
Scopes are fixed at design time and you cannot use them for dynamic purposes, such as different scopes for different types of user. Whenever you need to perform dynamic authorization, claims must be used.
Audience Checks
APIs should also check the audience of received access tokens, and the most common setup is for a set of related APIs to use the same audience. This enables JWT access tokens to be forwarded between microservices:
API | Audience |
---|---|
Orders | api.mycompany.com |
Customers | api.mycompany.com |
Products | api.mycompany.com |
If you deal with different subdivisions of a large company, then it is usually recommended to use a different audience per subdivision.
Step 3: Claims
Our next code sample will authorize using the following custom claims. Note that none of this data is stored in the Authorization Server:
Claim | Represents |
---|---|
User ID | The user id with which transactions are stored in the domain specific data |
User Role | Two user roles are involved, for a normal user and an administrative user |
User Regions | An administrator grants users access to data for one or more regions, represented as an array |
The third of these is an array claim, to represent the type of authorization that must be done in many real world business systems:
- A doctor might only see data for particular surgeries
- A coverage banker might only see data for particular industry sectors
- A retail worker might only see data for particular branches
Claims Requirements
There are two main requirements that you need to consider when working with claims, and we will show two main ways to achieve this:
Requirement | Description |
---|---|
API Data | The API must receive the data it needs in order to implement its domain specific authorization |
Confidentiality | Access tokens returned to internet clients must not reveal all of this information |
Claims Architecture Option 1
The ideal way to meet the above requirements is for the Authorization Server to reach out to the API at the time of token issuance to get custom claims, then to include the custom claims in the access token.
This state is then stored in the Authorization Server, and the access token returned to clients uses a confidential reference token format, which is typically a UUID or something similar:
When a client calls the API, the reference token is introspected to get a JWT access token, which is then forwarded to the API. The introspection is usually done in an API gateway that is placed in front of the API:
This is a great solution when supported, since all claims issued are audited by the Authorization Server and it scales very well if the JWT needs to be forwarded between microservices.
Claims Architecture Option 2
Option 1 may require a specialist Authorization Server, whereas this blog is using AWS Cognito by default, which does not support the above features.
We will therefore also demonstrate an alternative approach, where custom claims are looked up when an access token is first received, then cached:
All subsequent requests with the same access token can then be handled in a fast manner by quickly looking up extra claims from the memory cache:
A JWT access token issued by AWS Cognito for our second code example contains the following data. The access token is pretty confidential, so we are meeting that requirement:
Claims Principal
All of this blog’s APIs collect claims into a ClaimsPrincipal class. This should be designed early, to provide the information the API needs for authorization:
Authorizer Class
When using the Option 2 architecture we will use an Authorizer class to perform the claims lookup, populate the Claims Principal, and to deal with caching results:
Claims Caching Behaviour
Our next code sample will include some logging to demonstrate the above pattern. This highlights how **Claims Lookup **is only done when a new access token is received, and is very fast on subsequent requests:
The claims cache has the following characteristics:
- Each cache entry will have a key equal to a hash of the access token, so that getting a new access token forces a new claims lookup
- The lifetime of the cache entry must never exceed the expiry time of the received access token, and we will use an upper limit of 30 minutes
This means that if a user’s permissions change, they can log off and log on again to get a new access token, after which their updated privileges will come into effect.
Business Authorization
The claims principal, or its domain specific claims, can then be injected into business logic classes:
The API can then deny access if unauthorized resources are requested, or filter collections to only include authorized items:
User Info Claims
I prefer to also avoid including sensitive data in ID tokens, which sit around in a UI for an entire user session. If possible I instead serve User Info via APIs, which is both more secure and more extensible:
Step | Description |
---|---|
Confidential ID Tokens | Avoid including personally identifiable information in ID tokens |
Avoid Exposing OAuth User Info | Avoid exposing the OAuth User Info endpoint to the internet, to prevent anyone with an access token being able to get PII |
UIs call APIs to get User Info | APIs can return both OAuth User Info and domain specific user data to UI clients |
Where Are We?
We have described patterns for achieving Extensible Authorization in OAuth secured APIs. This blog’s next code sample will implement the claims caching pattern, and our final APIs will support both patterns.